Introducción

Motivación del trabajo

El trabajo se basa en un fichero csv extraido del sitio web KAGGLE llamado “sleep_health_lifestyle_dataset.csv”. Este fichero contiene una serie de columnas que las usaremos para intentar predecir enfermedades o la no tenencia de una sobre el sueño. Esa variable se encuentra en la última columna.

Lectura y visualización inicial

Comienzo con la lectura de los datos desde el fichero csv:

datos <- read_csv("sleep_health_lifestyle_dataset.csv")

A continuación pasaremos a una visualización del DataFrame para hacernos un poco la idea las distintas variables que tiene, que trabajo tendremos que acometer a lo largo del proyecto en cuestión de depuración de datos si hiciera falta, que representaciones gráficas pudieran resultar de interés.

head(datos)
# A tibble: 6 × 13
  `Person ID` Gender   Age Occupation    `Sleep Duration (hours)`
        <dbl> <chr>  <dbl> <chr>                            <dbl>
1           1 Male      29 Manual Labor                       7.4
2           2 Female    43 Retired                            4.2
3           3 Male      44 Retired                            6.1
4           4 Male      29 Office Worker                      8.3
5           5 Male      67 Retired                            9.1
6           6 Female    47 Student                            6.1
# ℹ 8 more variables: `Quality of Sleep (scale: 1-10)` <dbl>,
#   `Physical Activity Level (minutes/day)` <dbl>,
#   `Stress Level (scale: 1-10)` <dbl>, `BMI Category` <chr>,
#   `Blood Pressure (systolic/diastolic)` <chr>, `Heart Rate (bpm)` <dbl>,
#   `Daily Steps` <dbl>, `Sleep Disorder` <chr>

Como podemos ver de un primer vistazo, nos encontramos con la primera columna que es un id, y por lo tanto podemos prescindir de el tanto para la visualización como para lo modelos. Posteriormente nos encontramos variables como genero y edad cuyo estudio puede ser interesante si mujeres u hombres sufren mas o menos enfermedades del sueño, o si estas se ven afectadas por la franja de edad donde se encuentre la persona. Luego una variable sobre el trabajo que desarrolla la persona que también resulta interesante para los mismos estudios que antes. Tras esto tenemos variables mas relacionadas con el sueño como el numero de horas que duerme la persona o la calidad del sueño que seguro serán muy importantes para la fase de los modelos. Continuando, nos encontramos con una variable, la presión sanguínea que vemos que en una misma variable agrupa dos informaciones, la presión baja y alta separadas por una barra, por lo que como depuración tendremos que separar esta variable en otras dos.

summary(datos)
   Person ID        Gender               Age         Occupation       
 Min.   :  1.0   Length:400         Min.   :18.00   Length:400        
 1st Qu.:100.8   Class :character   1st Qu.:29.00   Class :character  
 Median :200.5   Mode  :character   Median :40.00   Mode  :character  
 Mean   :200.5                      Mean   :39.95                     
 3rd Qu.:300.2                      3rd Qu.:49.00                     
 Max.   :400.0                      Max.   :90.00                     
 Sleep Duration (hours) Quality of Sleep (scale: 1-10)
 Min.   : 4.100         Min.   : 1.000                
 1st Qu.: 5.900         1st Qu.: 4.700                
 Median : 8.200         Median : 6.100                
 Mean   : 8.041         Mean   : 6.126                
 3rd Qu.:10.125         3rd Qu.: 7.425                
 Max.   :12.000         Max.   :10.000                
 Physical Activity Level (minutes/day) Stress Level (scale: 1-10)
 Min.   : 10.00                        Min.   : 1.000            
 1st Qu.: 35.00                        1st Qu.: 3.000            
 Median : 65.50                        Median : 5.000            
 Mean   : 64.98                        Mean   : 5.473            
 3rd Qu.: 94.00                        3rd Qu.: 8.000            
 Max.   :120.00                        Max.   :10.000            
 BMI Category       Blood Pressure (systolic/diastolic) Heart Rate (bpm)
 Length:400         Length:400                          Min.   : 50.00  
 Class :character   Class :character                    1st Qu.: 63.00  
 Mode  :character   Mode  :character                    Median : 77.00  
                                                        Mean   : 75.99  
                                                        3rd Qu.: 90.00  
                                                        Max.   :100.00  
  Daily Steps    Sleep Disorder    
 Min.   : 2067   Length:400        
 1st Qu.: 6165   Class :character  
 Median :11786   Mode  :character  
 Mean   :11077                     
 3rd Qu.:15878                     
 Max.   :19958                     

De esta manera vemos el tipo de datos que tiene cada variable por si es necesario cambiarlo, junto con algunos datos interesantes de las variables.

Depuración de los datos

Eliminación variables que sobran

Continuamos con la adecuación de los datos que nos proporciona el DataFrame para poder representarlo gráficamente y construir modelos adecuadamente.

Nuestro primer paso, como comentamos nada mas visualizar los datos es eliminar la primera variable:

datos %<>%
  select(-`Person ID`)

Cambiar tipo de las variables

  1. Variable Gender
datos$Gender <- datos %$%
  Gender %>%
  as.factor()
summary(datos$Gender)
Female   Male 
   201    199 
  1. Variable BMI Category
datos$`BMI Category` <- datos %$%
  `BMI Category` %>%
  as.factor()
summary(datos$`BMI Category`)
     Normal       Obese  Overweight Underweight 
         91          98         109         102 
  1. Variable Occupation
datos$Occupation <- datos %$%
  Occupation %>%
  as.factor()
summary(datos$Occupation)
 Manual Labor Office Worker       Retired       Student 
           96            99            95           110 
  1. Variable Sleep Disorder
datos$`Sleep Disorder` <- datos %$%
  `Sleep Disorder` %>%
  as.factor()
summary(datos$`Sleep Disorder`)
   Insomnia        None Sleep Apnea 
         79         290          31 

Transformacion de la variable Blood Pressure

A continuación pasamos a cambiar la variable Blood Pressure. Como hemos comentado al comienzo, esta variable contiene la presión alta y baja en una sola y separada por la barra “/”. Lo que haremos es dividir esta variable en otras dos que ya renombraremos separando por esa barra.

# datos %$%
#   `Blood Pressure (systolic/diastolic)` %>%
#   str_split(.,pattern="/",simplify = T) 

Así tendríamos dividida la variable en dos columnas, una con la presión alta (la primera) y la otra con la presión baja. Pasamos a guardar cada una como variables y a eliminar esa variable que no nos sirve, haciendo todo el proceso de continuo.

Press_High <- datos %$%
  `Blood Pressure (systolic/diastolic)` %>%
  str_split(.,pattern="/",simplify = T) %>%
  .[,1] %>%
  as.numeric()
Press_Low <- datos %$%
  `Blood Pressure (systolic/diastolic)` %>%
  str_split(.,pattern="/",simplify = T) %>%
  .[,2] %>%
  as.numeric()
datos$Press_High <- Press_High
datos$Press_Low <- Press_Low

Ahora solo nos queda eliminar la columna que hemos transformado.

datos %<>%
  select(-`Blood Pressure (systolic/diastolic)`)
summary(datos)
    Gender         Age                Occupation  Sleep Duration (hours)
 Female:201   Min.   :18.00   Manual Labor : 96   Min.   : 4.100        
 Male  :199   1st Qu.:29.00   Office Worker: 99   1st Qu.: 5.900        
              Median :40.00   Retired      : 95   Median : 8.200        
              Mean   :39.95   Student      :110   Mean   : 8.041        
              3rd Qu.:49.00                       3rd Qu.:10.125        
              Max.   :90.00                       Max.   :12.000        
 Quality of Sleep (scale: 1-10) Physical Activity Level (minutes/day)
 Min.   : 1.000                 Min.   : 10.00                       
 1st Qu.: 4.700                 1st Qu.: 35.00                       
 Median : 6.100                 Median : 65.50                       
 Mean   : 6.126                 Mean   : 64.98                       
 3rd Qu.: 7.425                 3rd Qu.: 94.00                       
 Max.   :10.000                 Max.   :120.00                       
 Stress Level (scale: 1-10)      BMI Category Heart Rate (bpm)  Daily Steps   
 Min.   : 1.000             Normal     : 91   Min.   : 50.00   Min.   : 2067  
 1st Qu.: 3.000             Obese      : 98   1st Qu.: 63.00   1st Qu.: 6165  
 Median : 5.000             Overweight :109   Median : 77.00   Median :11786  
 Mean   : 5.473             Underweight:102   Mean   : 75.99   Mean   :11077  
 3rd Qu.: 8.000                               3rd Qu.: 90.00   3rd Qu.:15878  
 Max.   :10.000                               Max.   :100.00   Max.   :19958  
     Sleep Disorder   Press_High      Press_Low    
 Insomnia   : 79    Min.   :109.0   Min.   :60.00  
 None       :290    1st Qu.:115.0   1st Qu.:66.00  
 Sleep Apnea: 31    Median :122.0   Median :73.00  
                    Mean   :122.2   Mean   :73.04  
                    3rd Qu.:128.0   3rd Qu.:79.00  
                    Max.   :145.0   Max.   :96.00  

De esta manera Ya tenemos todas las variables de manera adecuada para la representación gráfica de estudios que nos interesen para una posterior construcción de modelos.

Exportación de los datos

Antes de terminar exportamos en un fichero nuevo los datos depurados para posibles consultas. Recordamos que para exportarlos, los datos que están convertidos a factor debemos pasarlos a tipo character:

datos2 <- datos %>%
  mutate(across(where(is.factor),as.character))
datos2[datos2$`Sleep Disorder`=="None","Sleep Disorder"]="No enfermedad"
write_csv(x = datos2,file = "Datos_depurados.csv",col_names = T)

Presion Arterial (Alta y baja)

Influencia de la presión arterial

datos %>%
  ggplot(aes(Press_High,Press_Low,color=`Sleep Disorder`)) +
  geom_point()

En esta primera gráfica comparamos la presión alta y la presión baja en función del tipo de enfermedad del sueño que tenga la persona. Una vez hecha la representación no podemos inferir gran cosa a partir del gráfico, puesto que todas las enfermedades están mas o menos igualmente representadas para valores similares de presión arterial

datos %>%
  ggplot(aes(Press_High,Press_Low,color=`Sleep Disorder`)) +
  geom_point() +
  geom_smooth(aes(linetype = `Sleep Disorder`))

Como podemos observar las linea que interpola cada conjunto de puntos en función de la clase de enfermedad del sueño que tenga es muy similar. Y hay que recalcar que los intervalos de confianza se cortan por lo que podemos descartar que estas variables nos sirvan para identificar que tipo de enfermedad posee la persona

Pulsaciones por minuto

Influencia de las pulsaciones

datos %>%
  ggplot() +
  stat_summary(aes(`Sleep Disorder`,`Heart Rate (bpm)`),
               fun=median,
               fun.min = min,
               fun.max = max)

Al igual que en el caso anterior parece que vuelven a ser muy similares las pulsaciones independientemente de la enfermedad que presente el individuo. Lo único que podríamos destacar es que los individuos con apnea del sueño la tienen algo mas alta

Índice de masa corporal

Influencia del BMI

En esta gráfica si que parece que podemos sacar alguna que otra conclusión mas: en el caso de las personas que sufren insomnio, hay una mayor proporción de personas que están demasiado delgados; en el caso de las personas que no sufren ninguna enfermedad parece que hay mayor proporción e personas que sufren sobrepeso. Por otro lado de las personas que sufren apnea del sueño parece que tienen igual proporción.

Con esta gráfica podemos ver mas claro el diagrama anterior. Se hace notar mas que las personas con un indice de masa normal están representadas en menor proporción en el conjunto de personas sin enfermedad del sueño

Ocupación laboral e indice de masa corporal

Influencia conjunta de ambas variables

datos %>%
  count(Occupation,`Stress Level (scale: 1-10)`,`Sleep Disorder`) %>%
  group_by(Occupation,`Sleep Disorder`) %>%
  mutate(Proportion = n/sum(n)) %>%
  ggplot(aes(`Stress Level (scale: 1-10)`,Proportion,fill=`Sleep Disorder`))+
  geom_col(position = 'dodge') + 
  scale_y_continuous(labels = scales::percent_format()) +
  facet_wrap(~Occupation,nrow = 2)

En este gráfico comparamos por la ocupación que tienen las personas y su enfermedad (o ausencia de ella) el nivel de estrés que dicen tener por si esto pudiera tener un comportamiento específico. Por ejemplo, para las personas que trabajan en labores manuales, los que sufren apnea del sueño son los que en mayor proporción tienen altos niveles de estrés. Lo mismo ocurre para el caso de los estudiantes, que en gran proporción los que dicen sufrir niveles altos de estrés sufren apnea del sueño. En cambio, las personas que sufren insomnio, en cualquier ocupación laboral, parece que se encuentran en mayor porcentaje en niveles de estrés bajos, mientras que las personas que no sufren ninguna enfermedad del sueño parecen estar repartidos los porcentajes entre los distintos niveles de estrés para cualquier ocupación

Actividad física y duración del sueño

Influencia conjunta de ambas variables

datos %>%
  ggplot(aes(`Sleep Duration (hours)`,`Quality of Sleep (scale: 1-10)`,color=`Sleep Disorder`)) +
  geom_point()

Volvemos a encontrarnos una situación en la que no podemos sacar demasiadas conclusiones puesto que no se observa un patrón en los datos. Lo mas reseñable es que la mayoría de puntos de personas con apnea del sueño tienen una calidad del sueño intermedio, ni valores muy altos ni muy bajos. Vamos a realizar una curva de regresión con su intervalo de confianza para asegurarnos de este patrón

datos %>%
  ggplot(aes(`Sleep Duration (hours)`,`Quality of Sleep (scale: 1-10)`,color=`Sleep Disorder`)) +
  geom_point() +
  geom_smooth(aes(linetype = `Sleep Disorder`))

Vemos que parece que estábamos en lo cierto que no podemos sacar conclusiones puesto que los tres intervalos de confianza se cortan y por tanto no podemos inferir que con una calidad del sueño mejor o pero o mas o menos horas de sueño pueda referirse a una persona con una determinada enfermedad del sueño.

División y preparación del conjunto de datos

A partir de este instante trabajamos con python.

Leemos y vemos una breve descripción de los datos para ver que no ha habido ningún problema:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 400 entries, 0 to 399
Data columns (total 13 columns):
 #   Column                                 Non-Null Count  Dtype  
---  ------                                 --------------  -----  
 0   Gender                                 400 non-null    object 
 1   Age                                    400 non-null    int64  
 2   Occupation                             400 non-null    object 
 3   Sleep Duration (hours)                 400 non-null    float64
 4   Quality of Sleep (scale: 1-10)         400 non-null    float64
 5   Physical Activity Level (minutes/day)  400 non-null    int64  
 6   Stress Level (scale: 1-10)             400 non-null    int64  
 7   BMI Category                           400 non-null    object 
 8   Heart Rate (bpm)                       400 non-null    int64  
 9   Daily Steps                            400 non-null    int64  
 10  Sleep Disorder                         400 non-null    object 
 11  Press_High                             400 non-null    int64  
 12  Press_Low                              400 non-null    int64  
dtypes: float64(2), int64(7), object(4)
memory usage: 40.8+ KB

Dividimos entre nuestra variable objetivo y las que usamos de predictoras:

Particionamos los datos en entrenamiento y test para la construcción de los modelos.

En las anteriores secciones nos hemos dado cuenta que hay muchos mas pacientes sin enfermedad que con apnea o insomnio. Veamos en términos porcentuales:

Sleep Disorder
No enfermedad    0.7250
Insomnia         0.1975
Sleep Apnea      0.0775
Name: proportion, dtype: float64

Como no tenemos la misma proporción en unas clases que en otras tenemos que separarlos intentando mantener las proporciones.

Dividimos las variables predictoras en las que son numéricas y las que son categóricas:

<class 'pandas.core.frame.DataFrame'>
Index: 300 entries, 161 to 357
Data columns (total 3 columns):
 #   Column        Non-Null Count  Dtype 
---  ------        --------------  ----- 
 0   Gender        300 non-null    object
 1   Occupation    300 non-null    object
 2   BMI Category  300 non-null    object
dtypes: object(3)
memory usage: 9.4+ KB
<class 'pandas.core.frame.DataFrame'>
Index: 300 entries, 161 to 357
Data columns (total 9 columns):
 #   Column                                 Non-Null Count  Dtype  
---  ------                                 --------------  -----  
 0   Age                                    300 non-null    int64  
 1   Sleep Duration (hours)                 300 non-null    float64
 2   Quality of Sleep (scale: 1-10)         300 non-null    float64
 3   Physical Activity Level (minutes/day)  300 non-null    int64  
 4   Stress Level (scale: 1-10)             300 non-null    int64  
 5   Heart Rate (bpm)                       300 non-null    int64  
 6   Daily Steps                            300 non-null    int64  
 7   Press_High                             300 non-null    int64  
 8   Press_Low                              300 non-null    int64  
dtypes: float64(2), int64(7)
memory usage: 23.4 KB

Ahora construimos nuestro pipeline para transformar los datos. Como en el apartado de visualización no hemos visto que haya ninguna variable que podamos decir que separa a nuestra variable objetivo hacemos un análisis en componentes prinicpales, para ver si reduciendo la dimensionalidad de los datos lo obtenemos de una mejor manera:

Ya tenemos los conjuntos de entrenamiento y test listos para crear los modelos y ver el rendimiento de los mismos

Modelo Naive-Bayes

Columna

Modelo

Una vez construimos el modelo pasamos a ver su efectividad comparando el resultado de pasar el conjunto test por el modelo con el verdadero valor de la variable objetivo:

GaussianNB()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Columna

Métricas del modelo

               precision    recall  f1-score   support

     Insomnia       0.00      0.00      0.00        20
No enfermedad       0.72      1.00      0.84        72
  Sleep Apnea       0.00      0.00      0.00         8

     accuracy                           0.72       100
    macro avg       0.24      0.33      0.28       100
 weighted avg       0.52      0.72      0.60       100

Matriz de confusión

Frontera de separación

Columna

Conclusiones

Modelo Random forest

Columna

Modelo

Aplicamos una malla para hallar los mejores hiperparámetros:
GridSearchCV(cv=15, estimator=RandomForestClassifier(random_state=47563),
             param_grid=[{'max_features': [2, 4, 6, 8],
                          'n_estimators': [3, 5, 7, 9]},
                         {'bootstrap': [True], 'max_features': [5],
                          'n_estimators': [5]}],
             return_train_score=True, scoring='accuracy')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Vemos ahora cuales son los mejores valores de hiperparámetros y por tanto cuál es el mejor modelo de Random Forest:

RandomForestClassifier(max_features=2, n_estimators=9, random_state=47563)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
RandomForestClassifier(max_features=2, n_estimators=9, random_state=47563)

Columna

Métricas del modelo

               precision    recall  f1-score   support

     Insomnia       0.17      0.15      0.16        20
No enfermedad       0.71      0.79      0.75        72
  Sleep Apnea       0.00      0.00      0.00         8

     accuracy                           0.60       100
    macro avg       0.29      0.31      0.30       100
 weighted avg       0.55      0.60      0.57       100

Matriz de confusión

Frontera de separación

Columna

Conclusiones

Modelo Decision Tree

Columna

Modelo

Vamos a pasarle al modelo otra vez una serie de hiperparámetros que sera la profundidad máxima del árbol para ver otra vez cual es la mejor profundidad de todas ellas y quedarnos con el mejor modelo:

GridSearchCV(cv=15, estimator=DecisionTreeClassifier(random_state=47563),
             param_grid=[{'max_depth': [3, 5, 7, 9, 11]}],
             return_train_score=True, scoring='accuracy')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Vemos ahora cuales son los mejores valores de hiperparámetros y por tanto cuál es el mejor modelo de Decision Tree:

DecisionTreeClassifier(max_depth=3, random_state=47563)

Columna

Métricas del modelo

               precision    recall  f1-score   support

     Insomnia       0.00      0.00      0.00        20
No enfermedad       0.72      0.99      0.84        72
  Sleep Apnea       0.00      0.00      0.00         8

     accuracy                           0.71       100
    macro avg       0.24      0.33      0.28       100
 weighted avg       0.52      0.71      0.60       100

Matriz de confusión

Frontera de separación

Columna

Conclusiones

Modelo KNN

Columna

Modelo

GridSearchCV(cv=15, estimator=KNeighborsClassifier(metric='hamming'),
             param_grid=[{'n_neighbors': [3, 5, 7, 9, 11]}],
             return_train_score=True, scoring='accuracy')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Vemos ahora cuales son los mejores valores de hiperparámetros y por tanto cuál es el mejor modelo de KNN:

KNeighborsClassifier(metric='hamming', n_neighbors=9)

Columna

Métricas del modelo

               precision    recall  f1-score   support

     Insomnia       0.20      1.00      0.33        20
No enfermedad       0.00      0.00      0.00        72
  Sleep Apnea       0.00      0.00      0.00         8

     accuracy                           0.20       100
    macro avg       0.07      0.33      0.11       100
 weighted avg       0.04      0.20      0.07       100

Matriz de confusión

Frontera de separación

Columna

Conclusiones

Modelo MLP

Columna

Modelo

GridSearchCV(cv=15, estimator=MLPClassifier(random_state=47563),
             param_grid=[{'activation': ['relu'],
                          'hidden_layer_sizes': [(5, 5), (5, 10), (5, 15),
                                                 (10, 5), (10, 10), (10, 15),
                                                 (15, 5), (15, 10), (15, 15)],
                          'max_iter': [500, 1000], 'solver': ['adam']}],
             return_train_score=True, scoring='accuracy')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Vemos ahora cuales son los mejores valores de hiperparámetros y por tanto cuál es el mejor modelo de KNN:

MLPClassifier(hidden_layer_sizes=(5, 5), max_iter=500, random_state=47563)

Nos quedamos con el parámetro con menos nodos en cada capa oculta y menor número de iteraciones. Pasamos a ver las métricas del modelo

Columna

Métricas del modelo

               precision    recall  f1-score   support

     Insomnia       0.00      0.00      0.00        20
No enfermedad       0.72      1.00      0.84        72
  Sleep Apnea       0.00      0.00      0.00         8

     accuracy                           0.72       100
    macro avg       0.24      0.33      0.28       100
 weighted avg       0.52      0.72      0.60       100

Matriz de confusión

Frontera de separación

Columna

Conclusiones